<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>12、Proxy——Proxy实例的方法</title>
	</head>
	<body>
		<div id="div"></div>
		<script type="text/javascript">
			//二、Proxy实例的方法
			//下面是上面这些拦截方法的详细介绍。
			//1.get()
			//get方法用于拦截某个属性的读取操作，可以接受三个参数，依次为目标对象、属性名和 proxy 实例本身（即this关键字指向的那个对象），其中最后一个参数可选。
			//get方法的用法，上文已经有一个例子，下面是另一个拦截读取操作的例子。
			var person = {
				name:'张三'
			};
			var proxy = new Proxy(person,{
				get:function(target,property){
					if(property in target){
						return target[property]
					}else{
						throw new ReferenceError("Property \""+property+"\" does not exist.");
					}
				}
			});
			console.log(proxy.name) //张三
//			console.log(proxy.age) //Uncaught ReferenceError: Property "age" does not exist.
			//上面代码表示，如果访问目标对象不存在的属性，会抛出一个错误。如果没有这个拦截函数，访问不存在的属性，只会返回undefined。
			//get方法可以继承
			let proto = new Proxy({},{
				get(target,propertyKey,receiver){
					console.log('GET '+propertyKey);
					return target[propertyKey];
				}
			});
			let obj = Object.create(proto);  
			obj.foo;  //GET foo
			//上面代码中，拦截操作定义在Prototype对象上面，所以如果读取obj对象继承的属性时，拦截会生效。
			//下面的例子使用get拦截，实现数组读取负数的索引。
			function createArray(...elements){
				let handler = {
					get(target,propKey,receiver){
						let index = Number(propKey);
						if(index < 0){
							propKey = String(target.length + index)
						}
						return Reflect.get(target,propKey,receiver);
					}
				};
				let target = [];
				target.push(...elements);
				return new Proxy(target,handler);
			}
			let arr = createArray('a','b','c');
			console.log(arr[-1])   //c
			//上面代码中，数组的位置参数是-1，就会输出数组的倒数最后一个成员。
			//利用 Proxy，可以将读取属性的操作（get），转变为执行某个函数，从而实现属性的链式操作。
			var pipe = (function(){
				return function(value){
					var funcStack = [];
					var oproxy = new Proxy({},{
						get:function(pipeObject,fnName){
							if(fnName === 'get'){
								return funcStack.reduce(function(val,fn){
									return fn(val)
								},value)
							}
							funcStack.push(window[fnName]);
							return oproxy;
						}
					});
					return oproxy;
				}
			}());
			var double = n => n*2;
			var pow = n => n*n;
			var reverseInt = n => n.toString().split('').reverse().join('') | 0;
			console.log(pipe(3).double.pow.reverseInt.get) //63
			//上面代码设置 Proxy 以后，达到了将函数名链式使用的效果。
			//下面的例子则是利用get拦截，实现一个生成各种 DOM 节点的通用函数dom。
			const dom = new Proxy({},{
				get(target,property){
					return function(attrs = {},...children){
						const el = document.createElement(property);
						for(let prop of Object.keys(attrs)){
							el.setAttribute(prop,attrs[prop]);
						}
						for(let child of children){
							if(typeof child === 'string'){
								child = document.createTextNode(child);
							}
							el.appendChild(child);
						}
						return el;
					}
				}
			});
			const el = dom.div({},
				'Hello,my name is',
				dom.a({href:'//example.com'},'Mark'),
				'. I like:',
				dom.ul({},
					dom.li({},'The web'),
					dom.li({},'Food'),
					dom.li({},'...actually that\'s it')
				)
			)
			document.getElementById('div').appendChild(el)
			//下面是一个get方法的第三个参数的例子
			const proxy_1 = new Proxy({},{
				get:function(target,property,receiver){
					return receiver;
				}
			});
			console.log(proxy_1.getReceiver === proxy_1)  //true
			//上面代码中，get方法的第三个参数receiver，总是为当前的 Proxy 实例。
			//如果一个属性不可配置（configurable）和不可写（writable），则该属性不能被代理，通过 Proxy 对象访问该属性会报错。
//			const target = Object.defineProperties({},{
//				foo:{
//					value:123,
//					writable:false,
//					configurable:false
//				}
//			})
//			const handler = {
//				get(target,propKey){
//					return 'abc';
//				}
//			}
//			const proxy_2 = new Proxy(target,handler);
//			proxy_2.foo   // TypeError: Invariant check failed

			
			//2.set()
			//set方法用来拦截某个属性的赋值操作，可以接受四个参数，依次为目标对象、属性名、属性值和 Proxy 实例本身，其中最后一个参数可选。
			//假定Person对象有一个age属性，该属性应该是一个不大于 200 的整数，那么可以使用Proxy保证age的属性值符合要求。
			let validator = {
				set: function(obj,prop,value){
					if(prop === 'age'){
						if(!Number.isInteger(value)){
							throw new TypeError('The age is not an integer');
						}
						if(value>200){
							throw new RangeError('The age seems invalid');
						}
					}
					obj[prop] = value;
				}
			};
			let person_1 = new Proxy({},validator);
			person_1.age = 100;
			console.log(person_1.age)  //100
//			person_1.age = 'young' //报错    Uncaught TypeError: The age is not an integer
//			person_1.age = 300; //Uncaught RangeError: The age seems invalid
		//上面代码中，由于设置了存值函数set，任何不符合要求的age属性赋值，都会抛出一个错误，这是数据验证的一种实现方法。利用set方法，还可以数据绑定，即每当对象发生变化时，会自动更新 DOM。
		//有时，我们会在对象上面设置内部属性，属性名的第一个字符使用下划线开头，表示这些属性不应该被外部使用。结合get和set方法，就可以做到防止这些内部属性被外部读写。
		const handler_1 = {
			get(target,key){
				invariant(key,'get');
				return target[key];
			},
			set(target,key,value){
				invariant(key,'set');
				target[key] = value;
				return true;
			}
		};
		function invariant(key,action){
			if(key[0] === '_'){
				throw new Error(`Invalid attempt to ${action} private "${key}" property`);
			}
		}
		const target_1 = {};
		const proxy_3 = new Proxy(target_1,handler_1);
//		proxy_3._prop  //Uncaught Error: Invalid attempt to get private "_prop" property
//		proxy_3._prop = 'c'; //Uncaught Error: Invalid attempt to set private "_prop" property
		//上面代码中，只要读写的属性名的第一个字符是下划线，一律抛错，从而达到禁止读写内部属性的目的。
		//下面是set方法第四个参数的例子。
		const handler_2 = {
			set:function(obj,prop,value,receiver){
				obj[prop] = receiver;
			}
		};
		const proxy_4 = new Proxy({},handler_2);
		proxy_4.foo = 'bar';
		console.log(proxy_4.foo === proxy_4)  //true
		//上面代码中，set方法的第四个参数receiver，总是返回this关键字所指向的那个对象，即proxy实例本身。
		//注意，如果目标对象自身的某个属性，不可写也不可配置，那么set不得改变这个属性的值，只能返回同样的值，否则报错。
		
		
		//3.apply()
		//apply方法拦截函数的调用、call和apply操作。
		//apply方法可以接受三个参数，分别是目标对象、目标对象的上下文对象（this）和目标对象的参数数组。
		var handler_3 = {
			apply(target,ctx,args){
				return Reflect.apply(...arguments);
			}
		};
		//例子：
		var target_2 = function(){
			return 'I am the target';
		};
		var handler_4 = {
			apply:function(){
				return 'I am the proxy';
			}
		};
		var p = new Proxy(target_2,handler_4);
		console.log(p())  //I am the proxy
		//上面代码中，变量p是 Proxy 的实例，当它作为函数调用时（p()），就会被apply方法拦截，返回一个字符串。
		//下面是另外一个例子。
		var twice = {
			apply(target,ctx,args) {
				return Reflect.apply(...arguments) * 2;
			}
		};
		function sum(left,right){
			return left + right;
		};
		var proxy = new Proxy(sum,twice);
		console.log(proxy(1,2))  //6
		console.log(proxy.call(null,5,6)) //22
		console.log(proxy.apply(null,[7,8]))  //30
		//上面代码中，每当执行proxy函数（直接调用或call和apply调用），就会被apply方法拦截。
		//另外，直接调用Reflect.apply方法，也会被拦截。
		console.log(Reflect.apply(proxy,null,[9,10]))  //38
		
		
		//4.has()
		//has方法用来拦截HasProperty操作，即判断对象是否具有某个属性时，这个方法会生效。典型的操作就是in运算符。
		//下面的例子使用has方法隐藏某些属性，不被in运算符发现。
		var handler_5 = {
			has(target,key) {
				if(key[0] === '_'){
					return false;
				}
				return key in target;
			}
		};
		var target_3 = {_prop:'foo',prop:'foo'};
		var proxy_2 = new Proxy(target_3,handler_5);
		console.log('_prop' in proxy_2)  //false
		//上面代码中，如果原对象的属性名的第一个字符是下划线，proxy.has就会返回false，从而不会被in运算符发现。
		//如果原对象不可配置或者禁止扩展，这时has拦截会报错。
		var obj_1 = {a:10};
		Object.preventExtensions(obj_1);
		var p = new Proxy(obj,{
			has:function(target,prop){
				return false;
			}
		});
//		console.log('a' in p)  //Uncaught SyntaxError: Identifier 'obj' has already been declared
		//上面代码中，obj对象禁止扩展，结果使用has拦截就会报错。也就是说，如果某个属性不可配置（或者目标对象不可扩展），则has方法就不得“隐藏”（即返回false）目标对象的该属性。
		//值得注意的是，has方法拦截的是HasProperty操作，而不是HasOwnProperty操作，即has方法不判断一个属性是对象自身的属性，还是继承的属性。
		//另外，虽然for...in循环也用到了in运算符，但是has拦截对for...in循环不生效。
		let stu1 = {name:'张三',score:59};
		let stu2 = {name:'李四',score:99};
		let handler_6 = {
			has(target,prop){
				if(prop === 'score' && target[prop] < 60){
					console.log(`${target.name} 不及格`);
					return false;
				}
				return prop in target;
			}
		}
		let oproxy1 = new Proxy(stu1,handler_6);
		let oproxy2 = new Proxy(stu2,handler_6);
		console.log('score' in oproxy1);
		// 张三  不及格
		// false
		console.log('score' in oproxy2)   //true
		for(let a in oproxy1){
			console.log(oproxy1[a])
		}
		//张三
		//59
		for(let b in oproxy2){
			console.log(oproxy2[b]);
		}
		//李四
		//99
		//上面代码中，has拦截只对in循环生效，对for...in循环不生效，导致不符合要求的属性没有被排除在for...in循环之外。
		
		
		//5.construct()
		//construct方法用于拦截new命令，下面是拦截对象的写法。
		var handler_7 = {
			construct(target,args,newTarget){
				return new target(...args);
			}
		};
		//construct方法可以接受两个参数
	    //target: 目标对象
	    //args：构建函数的参数对象
		//例子：
		var p = new Proxy(function(){},{
			construct:function(target,args){
				console.log('called:'+args.join(','));
				return {value:args[0]*10};
			}
		});
		console.log((new p(1)).value)
		//called:1
		//10
		//construct方法返回的必须是一个对象，否则会报错。
//		var p_1 = new Proxy(function(){},{
//			construct:function(target,argumentsList){
//				return 1;
//			}
//		});
//		console.log(new p_1()) //Uncaught TypeError: 'construct' on proxy: trap returned non-object ('1')

		
		//6.deleteProperty()
		//deleteProperty方法用于拦截delete操作，如果这个方法抛出错误或者返回false，当前属性就无法被delete命令删除。
		var handler_8 = {
			deleteProperty(target,key){
				invariant(key,'delete');
				return true;
			}
		};
		function invariant(key,action){
			if(key[0] === '_'){
				throw new Error(`Invalid attempt to ${action} private "${key}" property`);
			}
		}
		var target_4 = {_prop:'foo'};
		var proxy_2 = new Proxy(target_4,handler_8);
//		console.log(delete proxy_2._prop) //Uncaught Error: Invalid attempt to delete private "_prop" property
		//上面代码中，deleteProperty方法拦截了delete操作符，删除第一个字符为下划线的属性会报错。
		//注意，目标对象自身的不可配置（configurable）的属性，不能被deleteProperty方法删除，否则报错。
		
		
		//7.defineProperty()
		//defineProperty方法拦截了Object.defineProperty操作。
		var handler_9 = {
			defineProperty (target,key,descriptor){
				return false;
			}
		};
		var target_5 = {};
		var proxy_5 = new Proxy(target_5,handler_9);
		proxy_5.foo = 'bar'
		//上面代码中，defineProperty方法返回false，导致添加新属性会抛出错误。
		//注意，如果目标对象不可扩展（extensible），则defineProperty不能增加目标对象上不存在的属性，否则会报错。另外，如果目标对象的某个属性不可写（writable）或不可配置（configurable），则defineProperty方法不得改变这两个设置。
		
		
		//8.getOwnPropertyDescriptor()
		//getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor()，返回一个属性描述对象或者undefined。
		var handler_10 = {
			getOwnPropertyDescriptor (target,key){
				if(key[0] === '_'){
					return;
				}
				return Object.getOwnPropertyDescriptor(target,key);
			}
		};
		var target_6 = {_foo:'bar',baz:'tar'};
		var proxy_6 = new Proxy(target_6,handler_10);
		console.log(Object.getOwnPropertyDescriptor(proxy_6,'wat'))  //undefined
		console.log(Object.getOwnPropertyDescriptor(proxy_6,'_foo'))  //undefined
		console.log(Object.getOwnPropertyDescriptor(proxy_6,'baz'))
		//Object {value: "tar", writable: true, enumerable: true, configurable: true}
		//上面代码中，handler.getOwnPropertyDescriptor方法对于第一个字符为下划线的属性名会返回undefined。
		
		
		//9.getPrototypeOf()
		//getPrototypeOf方法主要用来拦截获取对象原型。具体来说，拦截下面这些操作。
//	    Object.prototype.__proto__
//	    Object.prototype.isPrototypeOf()
//	    Object.getPrototypeOf()
//	    Reflect.getPrototypeOf()
//	    instanceof
		//例子
		var proto_1 = {};
		var p_3 = new Proxy({},{
			getPrototypeOf(target){
				return proto_1;
			}
		});
		console.log(Object.getPrototypeOf(p_3) === proto_1)  //true
		//上面代码中，getPrototypeOf方法拦截Object.getPrototypeOf()，返回proto_1对象。
		//注意，getPrototypeOf方法的返回值必须是对象或者null，否则报错。另外，如果目标对象不可扩展（extensible）， getPrototypeOf方法必须返回目标对象的原型对象。
		
		
		//10.isExtensible()
		//isExtensible方法拦截Object.isExtensible操作。
		var p_4 = new Proxy({},{
			isExtensible:function(target){
				console.log('called');
				return true;
			}
		})
		console.log(Object.isExtensible(p_4))
		//called
		//true
		//上面代码设置了isExtensible方法，在调用Object.isExtensible时会输出called。
		//注意，该方法只能返回布尔值，否则返回值会被自动转为布尔值。
		//这个方法有一个强限制，它的返回值必须与目标对象的isExtensible属性保持一致，否则就会抛出错误。
//		var p_5 = new Proxy({},{
//			isExtensible:function(target){
//				return false;
//			}
//		});
//		Object.isExtensible(p_5)
		//Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
		
		
		//11.ownKeys()
		//ownKeys方法用来拦截对象自身属性的读取操作。具体来说，拦截以下操作
//	    Object.getOwnPropertyNames()
//	    Object.getOwnPropertySymbols()
//	    Object.keys()
		//下面是拦截Object.keys()的例子。
		let target_7 = {
			a:1,
			b:2,
			c:3
		};
		let handler_11 = {
			ownKeys(target){
				return ['a'];
			}
		};
		let proxy_7 = new Proxy(target_7,handler_11);
		console.log(Object.keys(proxy_7))  //["a"]
		//上面代码拦截了对于target_7对象的Object.keys()操作，只返回a、b、c三个属性之中的a属性。
		//下面的例子是拦截第一个字符为下划线的属性名。
		let target_8 = {
			_bar:'foo',
			_prop:'bar',
			prop:'baz'
		};
		let handler_12 = {
			ownKeys(target_8){
				return Reflect.ownKeys(target_8).filter(key => key[0] !== '_');
			}
		};
		let proxy_8 = new Proxy(target_8,handler_12);
		for(let key of Object.keys(proxy_8)){
			console.log(target_8[key])
		} //baz
		//注意，使用Object.keys方法时，有三类属性会被ownKeys方法自动过滤，不会返回。
    	//目标对象上不存在的属性
    	//属性名为 Symbol 值
    	//不可遍历（enumerable）的属性
		let target_9 = {
			a:1,
			b:2,
			c:3,
			[Symbol.for('secret')]:'4'
		}
		Object.defineProperty(target_9,'key',{
			enumerable:false,
			configurable:true,
			writable:true,
			value:'static'
		});
		let handler_13 = {
			ownKeys(target){
				return ['a','d',Symbol.for('secret'),'key']
			}
		}
		let proxy_9 = new Proxy(target_9,handler_13);
		console.log(Object.keys(proxy_9))  //['a']
		//上面代码中，ownKeys方法之中，显式返回不存在的属性（d）、Symbol 值（Symbol.for('secret')）、不可遍历的属性（key），结果都被自动过滤掉。
		//ownKeys方法还可以拦截Object.getOwnPropertyNames()。
		var p_6 = new Proxy({},{
			ownKeys:function(target){
				return ['a','b','c'];
			}
		});
		console.log(Object.getOwnPropertyNames(p_6))  //["a", "b", "c"]
		//ownKeys方法返回的数组成员，只能是字符串或 Symbol 值。如果有其他类型的值，或者返回的根本不是数组，就会报错。
//		var obj_1 = {};
//		var p_7 = new Proxy(obj_1,{
//			ownKeys:function(target){
//				return [123,true,undefined,null,{},[]]
//			}
//		});
//		console.log(Object.getOwnPropertyNames(p_7))
		//Uncaught TypeError: 123 is not a valid property name
		//上面代码中，ownKeys方法虽然返回一个数组，但是每一个数组成员都不是字符串或 Symbol 值，因此就报错了。
		//如果目标对象自身包含不可配置的属性，则该属性必须被ownKeys方法返回，否则报错。
		var obj_2 = {};
		Object.defineProperty(obj_2,'a',{
			configurable:false,
			enumerable:true,
			value:10
		});
		var p_8 = new Proxy(obj_2,{
			ownKeys:function(target){
				return ['b']
			}
		});
//		console.log(Object.getOwnPropertyNames(p_8)) //Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'
		//上面代码中，obj对象的a属性是不可配置的，这时ownKeys方法返回的数组之中，必须包含a，否则会报错。
		//另外，如果目标对象是不可扩展的（non-extensition），这时ownKeys方法返回的数组之中，必须包含原对象的所有属性，且不能包含多余的属性，否则报错。
		var obj_3 = {
			a:1
		};
		Object.preventExtensions(obj_3);
		var p_9 = new Proxy(obj_3,{
			ownKeys:function(target){
				return ['a','b']
			}
		})
//		console.log(Object.getOwnPropertyNames(p_9));
		//Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible
		//上面代码中，obj对象是不可扩展的，这时ownKeys方法返回的数组之中，包含了obj对象的多余属性b，所以导致了报错。
		
		
		//12.preventExtensions()
		//preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值，否则会被自动转为布尔值。
		//这个方法有一个限制，只有目标对象不可扩展时（即Object.isExtensible(proxy)为false），proxy.preventExtensions才能返回true，否则会报错。
		var p_10 = new Proxy({},{
			preventExtensions:function(target){
				return true;
			}
		});
//		console.log(Object.preventExtensions(p_10));
		//Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
		//上面代码中，proxy.preventExtensions方法返回true，但这时Object.isExtensible(proxy)会返回true，因此报错。
		//为了防止出现这个问题，通常要在proxy.preventExtensions方法里面，调用一次Object.preventExtensions。
		var p_11 = new Proxy({},{
			preventExtensions:function(target){
				console.log('called');
				Object.preventExtensions(target);
				return true;
			}
		})
		Object.preventExtensions(p_11)
		//called
		//true
		
		
		//13.setPrototypeOf()
		//setPrototypeOf方法主要用来拦截Object.setPrototypeOf方法。
		//例子：
		var handler_14 = {
			setPrototypeOf(target,proto){
				throw new Error('Changing the prototype is forbidden');
			}
		}
		var proto_2 = {};
		var target_10 = function(){};
		var proxy_10 = new Proxy(target_10,handler_14);
		Object.setPrototypeOf(proxy_10,proto_2)
		//Uncaught Error: Changing the prototype is forbidden
		//上面代码中，只要修改target的原型对象，就会报错。
		//注意，该方法只能返回布尔值，否则会被自动转为布尔值。另外，如果目标对象不可扩展（extensible），setPrototypeOf方法不得改变目标对象的原型。
		</script>
	</body>
</html>
